Skip to main content

라우팅 심화

Route Group(라우트 그룹)

  • 여러 관련 경로를 그룹으로 묶어 구조를 더 명확하게 표현
  • 폴더 이름에 괄호()를 사용하여 URL 경로에 포함되지 않는다
(src/)app/(advertise)/layout.js
import "../globals.css";

export default function RootLayout({ children }) {
return (
<html lang="en">
<body style={{ width: "100vw", height: "100vh" }}>
<div>{children}</div>
</body>
</html>
);
}
(src/)app/(advertise)/page.js
"use client";

import { useRouter } from "next/navigation";

export default function HomePage() {
const router = useRouter();

const handleLinkPage = () => {
router.push("/content");
};

return (
<>
<h1>광고 페이지 입니다.</h1>
<button onClick={handleLinkPage}>광고 닫기</button>
</>
);
}
(src/)app/(main)/layout.js
export default function Layout({ children }) {
return (
<html>
<body>{children}</body>
</html>
);
}
(src/)app/(main)/content/page.js
export default function ContentPage() {
return <>내용 페이지 입니다.</>;
}
note
src/app
├── (advertise)
│ ├── layout.js
│ ├── loading.js
│ └── page.js
└── (main)
├── content
│ └── page.js
└── layout.js

위의 코드는 Next.js를 실행 시킨 후, 광고 페이지 버튼을 눌러 내용 페이지로 이동하게 하는 코드
폴더 구조를 보면 app의 page.js가 없음에도 불구하고 동작이 된다.
(advertise)(main)에서 advertise에만 page.js가 있다.
Next.js가 판단하여 page.js가 존재하는 라우트 그룹의 페이지를 렌더링 시킨다.
만약 라우트 그룹 중 page.js가 2개이상 있을 경우 에러를 발생

image

페이지가 렌더링이 되고 경로를 확인해보면 /advertise로 되어있지 않다.
즉, 괄호()로 감쌓여진 폴더는 경로를 무시하며, 라우트를 묶기 위한 구조로 활용된다.
대신 동일한 레벨의 두 영역으로 나뉘어질 경우, 각각의 layout.js을 작성해야된다.

광고 닫기버튼을 누르면 (main)/content의 page.js화면이 나오는 것을 확인할 수 있다.

image

Link를 통해 /content 경로를 설정하였음에도 (main)이 경로에 영향을 끼치지 않는다는 것을 확인

Parallel Routes(병렬 라우트)

  • 동일한 경로 레벨에서 여러 개의 라우트가 동시에 활성화되고 관리
  • 하나의 레이아웃에 여러 라우트가 동시에 렌더링
  • 병렬 라우트를 생성 시, 폴더명을 @폴더명으로 작성
(src/)app/page.js
export default async function Home({ props }) {
return <div>Home Page입니다.</div>;
}
(src/)app/layout.js
import "./globals.css";

export default function RootLayout({ children, info, content }) {
return (
<html lang="en">
<body style={{ width: "100vw", height: "100vh" }}>
<div>{children}</div>
<div>{info}</div>
<div>{content}</div>
</body>
</html>
);
}
(src/)app/loading.js
export default function AppLoading() {
return <>로딩 중...</>;
}
(src/)app/@info/page.js
export default async function InfoPage() {
await new Promise((resolve) => setTimeout(resolve, 5000));
return <div>Info Page 입니다.</div>;
}
(src/)app/@content/page.js
export default async function ContentPage() {
await new Promise((resolve) => setTimeout(resolve, 1000));
return <div>Conetent Page 입니다.</div>;
}
note
src/app
├── page.js (Home 페이지)
├── loading.js (loading 페이지)
├── layout.js (layout 페이지)
├── @info
│ └── page.js (info 페이지)
└── @content
└── page.js (content 페이지)

app경로에 info, content 라우팅 페이지를 정의해놓은 코드
infocontent가 병렬 라우팅인지 확인하기 위해 setTimeout을 통해 각각 렌더링 시간을 걸어두었다.

병렬 라우팅을 하기 위해서는 동일한 레벨에서 @폴더명으로 폴더를 만들고 안에 page.js를 생성해야된다.
@ + 폴더명을 **슬롯(slots)**이라 한다.

병렬 라우팅에 대한 slots 및 page.js가 생성되었다면, 이 병렬 라우팅을 렌더링 시켜야된다.
slots들은 부모의 layout.js에 접근이 가능하다.
즉, infocontent의 부모인 app영역의 layout.js에서 사용할 수 있다는 것이다.

export default function RootLayout({ children, info, content }) {}

layout.js 코드를 보면 props영역에 직접적인 slots명을 작성하면 병렬 라우팅으로 선언된 slots들의 page.js를 호출한다.


실행 후 0초
image

실행 후 1초 뒤
image

실행 후 5초 뒤
image

결과 이미지를 보면 병렬 라우팅을 처리하게 되면 위와 같이 독립적인 로드 및 렌더링이 된다.
즉, 병렬 라우팅은 서로 독립적인 데이터를 로드해야할 경우에 유용하다.

Intercepting Routes(가로채는 라우트)

  • 특정 라우트의 접근을 조건에 따라 제어하거나 변경할 수 있는 라우팅 기법
  • 주로 모달의 띄우기 목적에 사용
info

Intercepting Routes 파일 생성 Convention

  • (.)폴더명: 동일한 레벨의 세그먼트와 일치
  • (..)폴더명: 한 레벨 위의 세그먼트와 일치
  • (..)(..)폴더명: 두 레벨 위의 세그먼트와 일치
  • (...)폴더명: 루트 단위부터와의 세그먼트와 일치

폴더명은 반드시 Intercept route를 적용할 경로 이름과 똑같이 작성해야된다.

Intercepting Routes 예시 코드

(src)/app/layout.js
export default function RootLayout({ children, modal }) {
return (
<html lang="en">
<body>
{modal}
{children}
</body>
</html>
);
}
(src)/app/page.js
import Link from "next/link";

export default function HomePage() {
return (
<>
<h1>홈페이지 입니다.</h1>
<Link href="/image">이미지 확인하러 가기</Link>
</>
);
}
(src/)app/@modal/default.js
export default function ModalDefaultPage() {
return null;
}
(src/)app/@modal/(.)image/page.module.css
.modal {
background-color: #bababa;
padding: 2rem;
border-radius: 4px;
border: none;
box-shadow: 0 0 10px 0 #181817;
}

.backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.85);
display: flex;
justify-content: center;
align-items: center;
}
(src/)app/@modal/(.)image/page.js
"use client";

import { useRouter } from "next/navigation";
import classes from "./page.module.css";

export default function InterceptImage() {
const router = useRouter();
return (
<div className={classes.backdrop} onClick={router.back}>
<dialog className={classes.modal} open>
<img src="../favicon.ico" alt="Intercept Image" />
</dialog>
</div>
);
}
(src/)app/image/page.js
export default function ImagePage() {
return <img src="../favicon.ico" />;
}
note
src/app
├── page.js (Home 페이지)
├── layout.js (layout 페이지)
├── @modal
│ ├── (.)image
│ │ ├── page.module.css (intercepting css)
│ │ └── page.js (intercepting 페이지)
│ └── default.js (default 페이지)
└── image
└── page.js (image 페이지)

위의 코드는 버튼을 누를 경우, 모달창에 이미지를 띄우는 코드

HomePage에서 이미지 확인하러 가기 버튼을 누를 경우, /image경로로 이동한다.
이 때, image폴더의 page.js가 표현되어야지만, 동작을 해보면 /@modal/(.)image의 page.js 페이지가 표현된다.
/@modal/(.)image를 이용하여 Intercept Route를 선언하였기 때문이다.


@modal에 default.js의 용도는 무엇일까?

default.js에는 Next.js에서 사용하는 예약어이다.
기본적으로 해당 경로에 접근할 때 가장 먼저 렌더링되는 Component를 정의

export default function RootLayout({ children, modal }) {
return (
<html lang="en">
<body>
{modal}
{children}
</body>
</html>
);
}

실질적으로 렌더링과 동시에 modal이 동작하는 것이 아닌 특정 버튼을 클릭하였을 때, 해당 URL로 변경되며 modal이 뜨기 때문에
해당 경로에 처음 접근할 때 빈 화면을 렌더링하여 모달이 열리기 전에 기본 상태를 유지하기 위해 default.js를 사용.


image경로에 page.js가 왜 필요한가?

Intercept Route는 경로를 가로채서 특정 Component를 렌더링하는 기능
즉, Link 혹은 useRouter를 이용하게 되면 페이지가 가로채서 Intercept Route로 설정한 페이지가 렌더링된다.

만약, 사용자가 직접적으로 URL을 이용하여 /image경로를 진입하거나,
/image경로의 진입 후, 새로고침을 하게 된다면 Intercept Route가 발생하지 않고 /image/page.js가 렌더링이 된다.
Next.js에서 서버 사이드 렌더링 또는 정적 파일 제공의 원칙에 따르기 때문이다.


왜 Intercepting Route를 이용해서 modal을 표현해야되나?

@modal/(.)image/page.js의 코드를 image/page.js에 넣은 후, @modal폴더를 지우고 실행해보면 된다.

  • Intercept Route 적용 코드 image

  • Intercept Route 미적용 코드 image

위의 이미지와 같이 별도의 페이지로 렌더링이 된다.
즉, Intercept Route를 사용하는 이유는 사용자 경험 향상이 있으며, SEO 및 URL 일관성 유지에 매우 중요한 기술이다.

Catch-All Routes

  • 동적 경로를 처리하는 방법
  • URL의 여러 세그먼트를 하나의 경로로 캡처하여 단일 Component에서 처리할 수 있게 하는 기능
  • [...폴더명]으로 폴더를 생성
(src/)app/page.js
import Link from "next/link";

export default function HomePage() {
return (
<>
<h1>HomePage 입니다.</h1>
<Link href="/catch">페이지 이동</Link>
</>
);
}
(src/)app/[...slug]/page.js
export default function CatchAllRouterPage(params) {
console.log("Catch All Routes 확인", params);
return <h1>Catch All Router Page</h1>;
}
note

Catch All Routes에 대해 출력물을 확인하기 위한 코드
Catch All Routes를 사용하기 위해서는 [...slug]라는 폴더를 생성
[]는 동적 라우팅을 의미하며, ...은 모든 하위 경로를 캡쳐하는 것을 의미

app의 page.js에서 Link를 통해 /catch 경로로 이동을 한다.
/catch로 이동 후, params를 출력해보면 동적 라우팅과 똑같이 출력을 한다.

image

위와 같이 params를 통해 캡쳐된 경로 세그먼트인 catch가 출력된 것을 보인다.
Catch All Routes는 별도의 폴더를 생성하지 않고도 추가적인 경로도 접근이 가능하다.

/catch/all/routes라는 경로를 접근

image

위와 같이 params를 통해 캡쳐된 경로 세그먼트인 catch, all,routes가 출력된 것을 확인할 수 있다.
또한, [...slug]의 page.js 페이지를 렌더링 하는 것을 확인할 수 있다.

Optional Catch All Routes

  • Catch All Routes의 확장 개념
  • 지정된 경로 이외에 상위 경로도 캡쳐. 즉, 세그먼트가 없어도 매칭
  • [[...폴더명]]으로 폴더를 생성
(src/)app/page.js
import Link from "next/link";

export default function HomePage() {
return (
<>
<h1>HomePage 입니다.</h1>
<Link href="/catch">페이지 이동</Link>
</>
);
}
(src/)app/catch/[[...slug]]/page.js
export default function CatchAllRouterPage(params) {
console.log("Optional Catch All Routes 확인", params);
return <h1>Optional Catch All Router Page</h1>;
}
note

Optional Catch All Routes에 대해 출력물을 확인하기 위한 코드

app의 page.js에서 Link를 통해 /catch경로로 이동
catch폴더에 page.js를 생성하지 않았음에도 페이지 렌더링이 된다.
렌더링이 된 페이지는 [[...slug]]에 생성된 page.js이다.
/catch의 page.js가 없어도 하위 경로의 Optional Catch All Routes의 page.js를 찾아 렌더링한다.

image

위의 이미지는 /catch의 경로에 접근했을 때의 출력물
/catch경로에 접근하였지만 [[...slug]]의 page.js를 렌더링 하였기 때문에 출력하는 것을 확인할 수 있다.
하지만 [[...slug]]에 대한 라우팅이 아닌 /catch에 대한 라우팅이기 때문에 slug에 대한 세그먼트가 []으로 출력이 된다.

image

위의 이미지는 /catch/all/routes의 경로에 접근했을 때의 출력물
/catch는 Optional Catch All Routes를 통해 []으로 출력된 것을 확인
이외 /catch 하위 경로의 세그먼트들은 slug에 저장된 것을 확인할 수 있다.

danger

Optional Catch All Router는 최상단 경로에서 [[...폴더명]] 생성이 불가능
반드시 경로를 생성 후, Optional Catch All Router를 생성해야된다.